<?php 
/**
 * This file is part of DomAr.
 * 
 * DomAr is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * DomAr is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with DomAr.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * @link http://inditel.ee/
 * @copyright 2007-2009 Inditel Meedia OÜ
 * @author Oliver Leisalu <oliver@inditel.ee>
 */

/**
* Main parser
*
* Parsing order:
* <ul>
* <li> Parsing - reads file source and finds properties, methods etc. if class extends then extended class will be parsed, checked and saved.</li>
* <li> Checking - properties check that their data is ok and class is ready for saving </li>
* <li> Saving - new source class file is created and saved. </li>
* </ul>
* @todo File syntax checking before parsing
* 
* @package DomAr
* @subpackage Parser
*/
class DomArClassParser {
	
	/**
	* @var String className
	*/
	protected $name;
	/**
	* @var String Source class file path
	*/
	protected $file;
	/**
	* @var String Class table name
	*/
	protected $tableName;	
	/**
	* @var String Database into object will be saved. 
	*/
	protected $database;
	/**
	 * @var String Table engine [MyIsam|InnoDB]
	 */
	protected $engine = 'InnoDB';
	/**
	* @var DomArClassParserProperty[] Class properties
	*/
	protected $properties = array();	
	/**
	* @var DomArClassParserRecordProperty[] Class properties that will be written into record
	*/
	protected $recordProperties = array();
	/**
	* @var DomArClassParserMethod Class methods
	*/
	protected $methods = array();	
	/**
	* @var DomArClassParserRecordMethod Class record methods
	*/
	protected $recordMethods = array();	
	/**
	* @var Array
	*/
	protected $constants = array();	
	/**
	* @var DomArClassParserMethod
	*/
	protected $constructor;	
	/**
	* @var Array Class validators
	*/
	protected $validators = array();
	/**
	 * @var Array
	 */
	protected $indexes = array();
	/**
	* @var Array Class listeners
	*/
	protected $classListeners = array();
	/**
	* @var Array Property listeners
	*/
	protected $propertyListeners = array();
	/**
	* @var DomArClassParser
	*/
	protected $parentClass;
	/**
	* @var bool
	*/
	protected $isAbstract = false;
	/**
	 * List of interface names that class imlements
	 *
	 * @var Array
	 */
	protected $implements = array();
	
	/**
	* @var DomArClassParser[]
	* Already created classes will be given out to avoid extra parsing overhead.
	*/
	private static $classes;
	
	/**
	* Record class name suffic
	* @param String
	*/
	const RECORD_CLASSNAME_SUFFIX = 'Record';
	
	private function __construct( $className ) {
		$this->name = $className;
		$this->file = Dom::getClassPath( $className.'_DomAr' );
		$this->tableName = strtolower($className);
		if( file_exists($this->file) === false )
			throw new RuntimeException('Can\'t find '.$className.' class file!');
	}
	
	public static function get( $className ) {
		if( isset(self::$classes[$className]) === false) {
			self::$classes[$className] = new DomArClassParser( $className );
			self::$classes[$className]->parse();
		}
		return self::$classes[$className];
	}
	

	

	//--------------------------------------------------------------
	// ADD
	//--------------------------------------------------------------
	
		/**
	* @param String $type Listener type, like beforeSave etc.
	* @param String $method Method that will be called when event occurs
	* @param Array $arguments
	* @param String $propertyName 
	*/
	public function addListener( DomArClassParserListener $listener ) {
		
		$listener->setClassName( $this->name );
		
		if( $listener->propertyName !== NULL ) {
			$this->propertyListeners[$listener->propertyName][$listener->type][] = $listener;
		} else {
			$this->classListeners[$listener->type][] = $listener;
		}
	}
	/**
	* @param String $propertyName
	* @param String $validatorName
	* @param Array $args
	*/
	public function addValidator( $propertyName, $validatorName, array $args, $isMethod = false ) {
		switch( $validatorName ) {
			case '<>': $validatorName = 'ne'; break;
			case '>=': $validatorName = 'ge'; break;
			case '<=': $validatorName = 'le'; break;
			case '>': $validatorName = 'gt'; break;
			case '<': $validatorName = 'lt'; break;
		}
	
		$this->validators[$propertyName][$validatorName] = array('name'=>$validatorName, 'args'=>$args, 'isMethod'=>(bool)$isMethod );
	}
	
	
	
	
	
	//--------------------------------------------------------------
	// GET
	//--------------------------------------------------------------
	
	public function __get( $var ) {
		return $this->$var;
	}
	
	public function getAllRecordProperties() {
		return arrayMerge( ($this->parentClass ? $this->parentClass->getAllRecordProperties() : array()), $this->recordProperties );
	}
	
	public function getAllProperties() {
		return arrayMerge( ($this->parentClass ? $this->parentClass->getAllProperties() : array()), $this->properties );
	}
	
	public function getAllRecordMethods() {
		return arrayMerge( ($this->parentClass ? $this->parentClass->getAllRecordMethods() : array()), $this->recordMethods );
	}
	
	public function getAllMethods() {
		return arrayMerge( ($this->parentClass ? $this->parentClass->getAllMethods() : array()), $this->methods );
	}
	
	public function getAllClassListeners() {
		return arrayMerge( ($this->parentClass ? $this->parentClass->getAllClassListeners() : array()), $this->classListeners );
	}
	
	public function getAllPropertyListeners() {
		return arrayMerge( ($this->parentClass ? $this->parentClass->getAllPropertyListeners() : array()), $this->propertyListeners );
	}
	
	public function getAllValidators() {
		return arrayMerge( ($this->parentClass ? $this->parentClass->getAllValidators() : array()), $this->validators );
	}
	
	public function getLastConstructor() {
		if( $this->constructor === NULL ) {
			$constructor = $this->getAllMethods();
			return $constructor['construct'];
		} else
			return $this->constructor;
	}
	
	
	
	
	
	//--------------------------------------------------------------
	// PARSER
	//--------------------------------------------------------------
	
	/**
	* Parses doc comment that is written before class definition.
 	*/
	private function parseClassDocComment( $docComment, $line = 0 ) {

		foreach( DomArAnnotationParser::parse( $docComment, 'class', $this->file, $line ) as $command ) {
			switch( $command['handler'] ) {
				case '__tablename':
					$this->tableName = strtolower($command['tableName']);
				break;
				case '__database':
					$this->database = strtolower($command['database']);
				break;
				case '__index':
					if( $command['type'] == 'index')
						$this->indexes[] = array('type' => 'index', 'columns' => $command['fields']);
					else if( $command['type'] == 'unique' )
						$this->indexes[] = array('type' => 'unique', 'columns' => $command['fields']);
					else if( $command['type'] == 'fulltext' )
						$this->indexes[] = array('type' => 'fulltext', 'columns' => $command['fields']);
				break;
				case '__listener':
					$listener = new DomArClassParserListener( $command['listener'], $command['method'], $command['args'] );
					$this->addListener( $listener );
				break;
				case '__engine':
					$this->engine = $command['engine'];
				break;
				default:
					throw new RuntimeException('Unknown command for class <strong>'.$this->name.'</strong> on line <strong>'.$line.'</strong>. Command: '.implode(' ',$command));
				break;
			}
		}
	}
	/**
	* Starts parsing class source file.
	*/
	private function parse() {
		
		
		/*$ch = curl_init( str_replace( SITE_PATH, SITE_URL, $this->file ) );
		curl_setopt($ch, CURLOPT_HEADER, 0);
		curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 3);
		$sresult = curl_exec($ch);
		curl_close($ch);
		if( $result ) {
			//throw new RuntimeException( 'Syntax error in file '.$this->file."\n" );
		}*/
		
		// File syntax checking before parsing it. Doesn't work on windows
		// exec('php -l '.$this->file,$error,$code);
		// if( !($code == 0 or $code == '') )
		// 	throw new RuntimeException( 'Syntax error in file '.$this->file."\n".implode("\n",$error) );

		$tokens = token_get_all(file_get_contents($this->file));
		//debug($tokens);
		$nrOfTokens = count($tokens);
		
		$braceLevel = 0;
		$inClass = false;

		for( $i=0; $i<$nrOfTokens; $i++) {
			$token = $tokens[$i];
				
			// kui sügaval asume { ja } vahel.
			if( $tokens[$i] == '{' ) 
				$braceLevel++;
			else if( $tokens[$i] == '}' )  {
				$braceLevel--;
				if( $braceLevel == 0)
					$inClass = false;
			}
			
			
			
			
			// kas oleme leidnud mõne klassi?
			if( $braceLevel === 0 && $tokens[$i][0] == T_CLASS) {
				
				for( $t=$i+1; $t<$nrOfTokens; $t++) {
					// find if class name matches our class names
					if( $tokens[$t][0] == T_STRING ) {
						if( $tokens[$t][1] == $this->name ) {
							$inClass = true;
							
							if( $tokens[$t-4][0] == T_ABSTRACT ) {
								$this->isAbstract = true;
							}
							
							// find interfaces
							$classDocComment = array();
							for( $k=$t-6; $k<=$t+10; $k++) {
								if( $tokens[$k][0] == T_IMPLEMENTS ) {
									$z = $k;
									while( true ) {
										if( $tokens[$z][0] == T_STRING) {
											$this->implements[] = $tokens[$z][1];
										}
										if( $tokens[$z][0] == '{' )
											break;
										$z++;
									}
								}
							}
							// ---
							
							// find docComment for class.
							$classDocComment = array();
							for( $k=$t-1; $k>=$t-7; $k--) {
								if( $tokens[$k][0] == T_DOC_COMMENT )
									$this->parseClassDocComment( $tokens[$k][1], $tokens[$k][2] );
							}
							// ---
							
						}
						$i = $t+1;
						break;
					} 
				}
			}
			
			
			
			
			// Oleme leidnud failis meile sobiva klassi, parsime slele propertid ja meetodid.
			if( $inClass === true ) {
			
			
			
				// vaatame mis on parent classi nimi kui extendib kuhugi.
				if( $tokens[$i][0] == T_EXTENDS) {
					for( $t=$i+1; $t<$nrOfTokens; $t++) {
						if( $tokens[$t][0] == T_STRING ) {
							if( $tokens[$t][1] != 'DomArObject' ) {
								class_exists( $tokens[$t][1] );
								$this->parentClass = DomArClassParser::get( $tokens[$t][1] );
							}
							$s = $t;
							break;
						}
					}
				}
			
			
			
				// found public, private or protected so theres property or method.
				if( $tokens[$i][0] == T_FINAL or $tokens[$i][0] == T_PUBLIC or $tokens[$i][0] == T_PROTECTED or $tokens[$i][0] == T_PRIVATE) {
					
					$inMethod = false;
					for( $t=$i+1; $t<$nrOfTokens; $t++) {
						
						if( $inMethod === false ) {
							// found method
							if( $tokens[$t][0] == T_FUNCTION ) {
								$method = DomArClassParserMethod::parse( $this, $tokens, $i );
								if( $method instanceof DomArClassParserRecordMethod )
									$this->recordMethods[$method->name] = $method;
								else
									$this->methods[$method->name] = $method;
								break;
								
							// found property
							} else if( $tokens[$t][0] == T_VARIABLE ) {
								$property = DomArClassParserProperty::parse( $this, $tokens, $i );
	
								if( $property instanceof DomArClassParserRecordProperty )
									$this->recordProperties[$property->name] = $property;
								else
									$this->properties[$property->name] = $property;
								break;
							}
						}

					}
				}
				
				// found const
				if( $tokens[$i][0] == T_CONST ) {
					$constantCode = NULL;
					for( $t=$i; $t<$nrOfTokens; $t++) {
						$constantCode .= is_array($tokens[$t]) ? $tokens[$t][1] : $tokens[$t];
						if( $tokens[$t][0] == ';') break;
					}
					$const = NULL;
					$const->code = $constantCode;
					$this->constants[] = $const;
					$i = $t;
				}
			}
		}
		
		if( isset($this->methods['construct']) === true ) {
			$this->constructor = $this->methods['construct'];
		}
	}
	
	public function check() {
		foreach( $this->getAllRecordProperties() as $property ) {
			$property->prepare( $this );
		}
		
		// ------------------------------------------
		// check overwriting
		// ------------------------------------------
		if( $this->parentClass && $this->parentClass->isAbstract == false ) {
			$parentProperties = $this->parentClass->getAllRecordProperties();
			foreach( $parentProperties as $key => $property ) {
				if( isset($this->recordProperties[$key]) ) {
					throw new RuntimeException('Class <b>'.$this->name.'</b> extending from <b>'.$this->parentClass->name.'</b> can\'t overwrite property <b>'.$key.'</b>!');
				}
			}
		}
	}
	
	/**
	* Generates new class source file for including
	* @return String
	*/
	private function toSource() {
		$source = file_get_contents( dirname(__FILE__).'/../Parser/DomArObjectBase.source' );

		// ------------------------------------------
		// base data
		// ------------------------------------------
		$source = str_replace( '<$className>', $this->name, $source );
		$source = str_replace( '<$abstract>', $this->isAbstract ? 'abstract ' : NULL, $source );
		$source = str_replace( '<$implements>', $this->implements ? ' implements '.implode(',',$this->implements) : NULL, $source );
		$source = str_replace( '<$parentClassName>', $this->parentClass ? $this->parentClass->name : 'DomArObject', $source );
		$source = str_replace( '<$recordClassName>', $this->name.self::RECORD_CLASSNAME_SUFFIX, $source );
		$source = str_replace( '<$tableName>', $this->tableName, $source);
		$source = str_replace( '<$database>', $this->database, $source);
		$source = str_replace( '<$escapedDatabaseDot>', $this->database ? '`'.$this->database.'`.' : '' , $source);
		
		$removeFromGlobalCacheCode = '';
		if( is_a( $this->parentClassName,'DomArGlobalObject') )
			$removeFromGlobalCacheCode = 'DomCache::remove(\''.$this->name.'\'.$this->getId());';
		$source = str_replace( '<$removeFromGlobalCacheCode>', $removeFromGlobalCacheCode, $source);
		
		$indexes = '';
		foreach( $this->indexes as $index ) {
			$indexes .= "\t\t\t".'self::$table->addClassIndex(\''.$index['type'].'\', array(\''.implode('\',\'',$index['columns']).'\'));'."\n";
		}
		$indexes .= "\t\t\t".'self::$table->setEngine('.$this->engine.');'."\n";
		$source = str_replace( '<$modifyTable>', $indexes, $source);
		
		$this->toSource_constants( $source);

		$this->toSource_properties( $source );
		
		$this->toSource_validators( $source );
		
		$this->toSource_setters( $source );

		$this->toSource_getters( $source );

		$this->toSource_methods( $source);

		$this->toSource_listeners( $source );

		$this->toSource_constructor( $source );

		
		
		
		return $source;
	}
	
	private function toSource_listeners( &$source ) {
		$beforeInsert = '';
		$afterInsert = '';
		$beforeUpdate = '';
		$afterUpdate = '';
		$beforeDelete = '';
		$afterDelete = '';
		$pluginListenrs = '';
		foreach( $this->propertyListeners as $propertyName => $listeners ) {		
			foreach( $listeners as $type => $typeListeners ) {
				foreach( $typeListeners as $listener ) {
					switch( $type ) {
						case 'beforeinsert':
							$beforeInsert .= "\t\t".$listener->getPhpCode()."\n";
						break;
						case 'afterinsert':
							$afterInsert .= "\t\t".$listener->getPhpCode()."\n";
						break;
						
						case 'beforeupdate':
							$beforeUpdate .= "\t\t".'if( $this->_changed[\''.$listener->propertyName.'\'] === true) '.$listener->getPhpCode()."\n";
						break;
						case 'afterupdate':
							$afterUpdate .= "\t\t".'if( $changed[\''.$listener->propertyName.'\'] === true) '.$listener->getPhpCode()."\n";
						break;
						
						case 'beforesave':
							$beforeInsert .= "\t\t".$listener->getPhpCode();
							$beforeUpdate .= "\t\t".'if( $this->_changed[\''.$listener->propertyName.'\'] === true) '.$listener->getPhpCode()."\n";
						break;
						case 'aftersave':
							$afterInsert .= "\t\t".$listener->getPhpCode();
							$afterUpdate .= "\t\t".'if( $changed[\''.$listener->propertyName.'\'] === true) '.$listener->getPhpCode()."\n";
						break;
						
						case 'beforedelete':
							$beforeDelete .= "\t\t".$listener->getPhpCode()."\n";
						break;
						case 'afterdelete':
							$afterDelete .= "\t\t".$listener->getPhpCode()."\n";
						break;
						case 'beforevalidate':break;
						case 'beforeset':break;
						case 'afterset':break;
						// plugin listeners
						default:
							if( isset($this->recordProperties[$propertyName]) ) {
								$pluginListeners .= "\t\t\t".$listener->getPhpCode();
							}
						break;
					}
				}
			}
		}
		
		foreach( $this->classListeners as $type => $typeListeners ) {
			foreach( $typeListeners as $listener ) {
				switch( $type ) {
					case 'beforeinsert':
						$beforeInsert .= "\t\t".$listener->getPhpCode()."\n";			
					break;
					case 'afterinsert':
						$afterInsert .= "\t\t".$listener->getPhpCode()."\n";
					break;
					
					case 'beforeupdate':
						$beforeUpdate .= "\t\t".$listener->getPhpCode()."\n";			
					break;
					case 'afterupdate':
						$afterUpdate .= "\t\t".$listener->getPhpCode()."\n";	
					break;
					
					case 'beforesave':
						$beforeInsert .= "\t\t".$listener->getPhpCode()."\n";	
						$beforeUpdate .= "\t\t".$listener->getPhpCode()."\n";			
					break;
					case 'aftersave':
						$afterInsert .= "\t\t".$listener->getPhpCode()."\n";
						$afterUpdate .= "\t\t".$listener->getPhpCode()."\n";	
					break;
					
					case 'beforedelete':
						$beforeDelete .= "\t\t".$listener->getPhpCode()."\n";
					break;
					case 'afterdelete':
						$afterDelete .= "\t\t".$listener->getPhpCode()."\n";
					break;
					default:
						throw new RuntimeException('Unknown class listener type <strong>'.$type.'</strong> for class <strong>'.$this->name.'</strong>');
					break;
				}
			}
		}

		$source = str_replace( '<$beforeInsertListeners>', $beforeInsert, $source );
		$source = str_replace( '<$afterInsertListeners>', $afterInsert, $source );
		$source = str_replace( '<$beforeUpdateListeners>', $beforeUpdate, $source );
		$source = str_replace( '<$afterUpdateListeners>', $afterUpdate, $source );
		$source = str_replace( '<$beforeDeleteListeners>', $beforeDelete, $source );
		$source = str_replace( '<$afterDeleteListeners>', $afterDelete, $source );
		$source = str_replace( '<$pluginListeners>', $pluginListeners, $source );
		
		
		$beforeValidateListeners = 'switch( $propertyName ) {'."\n";
		$beforeSetListeners = 'switch( $propertyName ) {'."\n";
		$afterSetListeners = 'switch( $propertyName ) {'."\n";
		foreach( $this->recordProperties as $property ) {
			
			
			
			// add before beforeValidate Listeners
			if( $this->propertyListeners[$property->name]['beforevalidate'] ) {
				$beforeValidateListeners .= "\t\t\t".'case \''.$property->name.'\':'."\n";
				foreach( $this->propertyListeners[$property->name]['beforevalidate'] as $listener ) {
					$beforeValidateListeners .= "\t\t\t\t".$listener->getPhpCode()."\n";
				}
				$beforeValidateListeners .= "\t\t\t".'return;'."\n";
			}
			// -----------
			
			
			
			
			// add before set listeners
			if( $this->propertyListeners[$property->name]['beforeset'] ) {
				$beforeSetListeners .= "\t\t\t".'case \''.$property->name.'\':'."\n";
				foreach( $this->propertyListeners[$property->name]['beforeset'] as $listener ) {
					$beforeSetListeners .= "\t\t\t\t".$listener->getPhpCode()."\n";
				}
				$beforeSetListeners .= "\t\t\t".'return;'."\n";
			}
			// -----------

			
			
				
			// add after set listeners
			if( $this->propertyListeners[$property->name]['afterset'] ) {
				$afterSetListeners .= "\t\t\t".'case \''.$property->name.'\':'."\n";
				foreach( $this->propertyListeners[$property->name]['afterset'] as $listener ) {
					$afterSetListeners .= "\t\t\t\t".$listener->getPhpCode()."\n";
				}
				$afterSetListeners .= "\t\t\t".'return;'."\n";
			}
			// -----------
		}
		
		$beforeValidateListeners .= "\t\t".'}';
		$beforeSetListeners .= "\t\t".'}';
		$afterSetListeners .= "\t\t".'}';
		
		$source = str_replace( '<$beforeValidateListeners>', $beforeValidateListeners, $source );
		$source = str_replace( '<$beforeSetListeners>', $beforeSetListeners, $source );
		$source = str_replace( '<$afterSetListeners>', $afterSetListeners, $source );
	}
	
	private function toSource_constructor( &$source ) {
		$constructorArguments = '';
		$constructor = $this->getLastConstructor();
		if( $constructor != NULL ) {
			$allProperties = $this->getAllRecordProperties();
			foreach( $constructor->arguments as $argument ) {
				$recordProperty = $allProperties[$argument];
				
				
				$lazy = in_array('IDomArLazyProperty', class_implements($recordProperty->handlerClassName));
				$init = in_array('IDomArPropertyInitListener', class_implements($recordProperty->handlerClassName));
				
				if( $recordProperty === NULL )
					throw new RuntimeException('Unknown record property <strong>'.$argument.'</strong> for class <strong>'.$this->name.'</strong>! Constructor arguments can only be record properties!');
				else if( $init && $lazy )
					$constructorArguments .= ',self::getProperty(\''.$argument.'\')->load($item,self::getProperty(\''.$argument.'\')->init($item,$data))';
				else if( $lazy )
					$constructorArguments .= ',self::getProperty(\''.$argument.'\')->load($item,$data[\''.$argument.'\'])';
				else if( $init )
					$constructorArguments .= ',self::getProperty(\''.$argument.'\')->init($item,$data)';
				else
					$constructorArguments .= ',$data[\''.$argument.'\']';
			}	
		}
		$source = str_replace( '<$constructorArguments>', substr($constructorArguments,1), $source );
	}
	
	private function toSource_methods( &$source ) {
		// ------------------------------------------
		// class methods
		// ------------------------------------------
		$methods = "\n";
		foreach( $this->methods as $method ) {
			$methods .= "\t".$method->code."\n";
		}
		$source = str_replace( '<$methods>', $methods, $source );
		
		
		// ------------------------------------------
		// record methods
		// ------------------------------------------
		$recordMethods = "\n";
		foreach( $this->recordMethods as $method ) {
			$recordMethods .= "\t".$method->code."\n";
		}
		$source = str_replace( '<$recordMethods>', $recordMethods, $source );
	}
	
	private function toSource_getters( &$source ) {
		$getMethods = '';
		foreach( $this->recordProperties as $property ) {
			$getMethods .= "\t".'public function get'.ucfirst($property->name).'() {'."\n";
			
			//if( $property->access != 'public' )
			//	$getMethods .= '$this->analyzeAccess(\''.$property->name.'\');';

			$handlerImplements = class_implements( $property->handlerClassName );
			
			if( in_array('IDomArPropertyLoadListener', $handlerImplements) ) {
				$getMethods .= "\t\t".'if( self::$properties[\''.$property->name.'\']->isLoaded($this, $this->'.$property->name.') === false ) ';
				$getMethods .= "\t\t".'$this->'.$property->name.' = self::$properties[\''.$property->name.'\']->load( $this, $this->'.$property->name.' );'."\n";
			}
			
			$getMethods .= "\t\t".'return $this->'.$property->name.';'."\n";
			$getMethods .= "\t".'}'."\n";
			
		}
		$source = str_replace( '<$getMethods>', $getMethods, $source );
	}
	
	private function toSource_setters( &$source ) {
		/*
		What is done in setPROPERTY method.
			- checked if value is not same
			- call user before validate listeners
			- validate new value (also user validation methods)
			- call user before set listeners
			- property set listener will be invoked (if exists)
			- property is set to changed and value is updated.
			- call user after set listeners
		*/
		$setMethods = '';
		foreach( $this->recordProperties as $property ) {
			$setMethods .= "\t".'public function set'.ucfirst($property->name).'( $value ) {'."\n";
			
			if( $property->access != 'public' )
				$setMethods .= "\t\t".'$this->analyzeAccess(\''.$property->name.'\');'."\n";

			
			$propertyListener = in_array('IDomArPropertyCompareListener', class_implements( $property->handlerClassName ));
			if( $propertyListener === true ) {	
				$setMethods .= "\t\t".'if( self::$properties[\''.$property->name.'\']->compare($this, $this->get'.ucfirst($property->name).'(), $value ) ) return;'."\n";
			} else {
				$setMethods .= "\t\t".'if( $this->get'.ucfirst($property->name).'() === $value ) return;'."\n";
			}
			
			
			// add before beforeValidate Listeners
			if( $this->propertyListeners[$property->name]['beforevalidate'] ) {
				$setMethods .= "\t\t".'$this->_beforeValidate( \''.$property->name.'\', $value );'."\n";
			}
			// -----------
			
			
			
			$setMethods .= "\t\t".'$customParams=array();'."\n";
			$setMethods .= "\t\t".'if( ($e = $this->_validate(\''.$property->name.'\', $value, false, $customParams )) !== 0) {'."\n";
			$setMethods .= "\t\t\t".'$this->registerFailedValidation( $e, \''.$property->name.'\', $value, $customParams); '."\n\t\t\t".'return;'."\n\t\t".'}'."\n";
			
			
			
			// add before set listeners		
			if( $this->propertyListeners[$property->name]['beforeset'] ) {
				$setMethods .= "\t\t".'$this->_beforeSet( \''.$property->name.'\', $value );'."\n";
			}
			
			$propertyListener = in_array('IDomArPropertyBeforeSetListener', class_implements( $property->handlerClassName ));
			if( $propertyListener === true )
				$setMethods .= "\t\t".'$this->__get(\''.$property->name.'\'); self::$properties[\''.$property->name.'\']->beforeSet( $this, $this->'.$property->name.', $value );'."\n";
			
			// -----------
				
				
			$setMethods .= "\t\t".'$this->_changed[\''.$property->name.'\'] = true; '."\n\t\t".'$this->'.$property->name.' = $value;'."\n";
			
			
			// add after set listeners
			$propertyListener = in_array('IDomArPropertyAfterSetListener', class_implements( $property->handlerClassName ));
			if( $propertyListener === true )
				$setMethods .= "\t\t\t\t".'self::$properties[\''.$property->name.'\']->afterSet( $this, $this->'.$property->name.' );'."\n";
			
			if( $this->propertyListeners[$property->name]['afterset'] ) {
				$setMethods .= "\t\t".'$this->_afterSet( \''.$property->name.'\');'."\n";
			}
			// -----------
			
			$setMethods .= "\t".'}'."\n\n";
		}
		$source = str_replace( '<$setMethods>', $setMethods, $source );
		
	}
	
	private function toSource_validators( &$source ) {
		
		$validate .= 'if( $this->validationEnabled !== true) return 0;'."\n";
		$validate .= "\t\t".'switch( $propertyName ) {'."\n";
		foreach( $this->recordProperties as $property ) {
			
			$validate .= "\t\t\t".'case \''.$property->name.'\':'."\n";
			
			// property validator
			$validate .= "\t\t\t\t".'if( $onlyCustomValidators === false && ($e = self::$properties[\''.$property->name.'\']->validate($this, $value, $customParams)) !== 0 ) return $e;'."\n";
			
			// add user validators
			$allValidators = $this->getAllValidators();
			foreach( (array)$allValidators[$property->name] as $validator  ) {
				$validatorArgs = array();
				foreach( $validator['args'] as $arg ) {
					if( is_array($arg) ) {
						$validatorArgs[] = self::arrayToPhpFunctionArgs( $arg );
					} else if( $arg === 'NULL' ) {
						$validatorArgs[] = 'NULL';
					} else {
						$validatorArgs[] = (is_numeric($arg) ? $arg : '\''.$arg.'\'');
					}
				}
				$validatorArgs = (implode(',',$validatorArgs));
				$validatorArgs = $validatorArgs ? ','.$validatorArgs : NULL;
				if( $validator['isMethod'] == 1) {
					if( !$this->methods[$validator['name']] && !$this->recordMethods[$validator['name']] ) 
						throw new RuntimeException('Class <strong>'.$this->name.'</strong> doens\'t have validation method <strong>'.$validator['name'].'</strong>!');
					
					$validate .= "\t\t\t\t".'if( ($e = $this->'.$validator['name'].'( $value '.$validatorArgs.', $customParams)) !== 0 ) return $e;'."\n";
				} else {
					$validate .= "\t\t\t\t".'if( ($e = DomArValidator_'.ucfirst($validator['name']).'::validate( $value '.$validatorArgs.',$customParams) ) !== 0 ) return $e;'."\n";
				}
			}
			// -------------------
			
			$validate .= "\t\t\t\t".'return 0;'."\n";
		}
		$validate .= "\t\t".'}';
		$source = str_replace( '<$validate>', $validate, $source );
	}
	
	private function toSource_properties( &$source ) {
		
		// ------------------------------------------
		// class properties
		// ------------------------------------------
		$properties = "\n";
		foreach( $this->properties as $property ) {
			$properties .= "\t".$property->code."\n";
		}
		$source = str_replace( '<$properties>', $properties, $source );
		
		// ------------------------------------------
		// record properties
		// ------------------------------------------
		
		$recordProperties = "\n";
		$metadataProperties = "\n";
		$assignData = '';
		$beforeInsert = '';
		$insert = '';
		$afterInsert = '';
		$beforeUpdate = '';
		$update = '';
		$afterUpdate  = '';
		$beforeDelete = '';
		$afterDelete  = '';
		
		$constructor = $this->getLastConstructor();
		foreach( $this->recordProperties as $property ) {
			
			// record properties
			$recordProperties .= "\t".$property->code."\n";
			
			// metadata
			$metadataProperties .= "\t\t\t\t'".$property->name."'=>".$property->createHandlerCode.','."\n";
			
			// constructor arguments
			if( count($constructor->arguments) == 0 or in_array($property->name, $constructor->arguments) === false ) {
				if( in_array('IDomArPropertyInitListener', class_implements( $property->handlerClassName) ) )
					$assignData .= "\t\t".'$this->'.$property->name.' = self::$properties[\''.$property->name.'\']->init( $this, $data );'."\n";
				else
					$assignData .= "\t\t".'$this->'.$property->name.' = $data[\''.$property->name.'\'];'."\n";
			}
			
			// property listeners
			$handlerImplements = class_implements(  $property->handlerClassName );
			
			if( in_array('IDomArPropertyBeforeInsertListener', $handlerImplements) ) {
				$beforeInsert .= "\t\t\t".'$this->__get(\''.$property->name.'\');'."\n";
				$beforeInsert .= "\t\t\t".'self::$properties[\''.$property->name.'\']->beforeInsert( $session, $this, $this->'.$property->name.' );'."\n";
			}
			
			if( in_array('IDomArPropertyInsertListener', $handlerImplements) ) {
				$insert .= "\t\t".'self::$properties[\''.$property->name.'\']->insert( $this, $this->'.$property->name.', $set );'."\n";
			}
			
			if( in_array('IDomArPropertyAfterInsertListener', $handlerImplements) ) {
				$afterInsert .= "\t\t".'self::$properties[\''.$property->name.'\']->afterInsert( $this, $this->'.$property->name.' );'."\n";
			}
			
			
			if( in_array('IDomArPropertyBeforeUpdateListener', $handlerImplements) ) {
				$beforeUpdate .= "\t\t".'if( $this->_changed[\''.$property->name.'\'] === true ) self::$properties[\''.$property->name.'\']->beforeUpdate( $session, $this, $this->'.$property->name.' );'."\n";
			}
			
			if( in_array('IDomArPropertyUpdateListener', $handlerImplements) ) {
				$update .= "\t\t".'if( $changed[\''.$property->name.'\'] === true ) self::$properties[\''.$property->name.'\']->update( $this, $this->'.$property->name.', $set );'."\n";
			}
			
			if( in_array('IDomArPropertyAfterUpdateListener', $handlerImplements) ) {
				$afterUpdate .= "\t\t".'if( $changed[\''.$property->name.'\'] === true ) self::$properties[\''.$property->name.'\']->afterUpdate( $this, $this->'.$property->name.' );'."\n";
			}
			
			
			if( in_array('IDomArPropertyBeforeDeleteListener', $handlerImplements) ) {
				$beforeDelete .= "\t\t".'self::$properties[\''.$property->name.'\']->beforeDelete( $this, $this->__get(\''.$property->name.'\') );'."\n";
			}
			
			if( in_array('IDomArPropertyAfterDeleteListener', $handlerImplements) ) {
				$afterDelete .= "\t\t".'self::$properties[\''.$property->name.'\']->afterDelete( $this, $this->'.$property->name.' );'."\n";
			}
			// -------
		}
		$source = str_replace( '<$recordProperties>', $recordProperties, $source );
		$source = str_replace( '<$metadataProperties>', $metadataProperties, $source );
		$source = str_replace( '<$assignData>', $assignData, $source );
		$source = str_replace( '<$_beforeInsert>', $beforeInsert, $source );
		$source = str_replace( '<$_insert>', $insert, $source );
		$source = str_replace( '<$_afterInsert>', $afterInsert, $source );
		$source = str_replace( '<$_beforeUpdate>', $beforeUpdate, $source );
		$source = str_replace( '<$_update>', $update, $source );
		$source = str_replace( '<$_afterUpdate>', $afterUpdate, $source );
		$source = str_replace( '<$_beforeDelete>', $beforeDelete, $source );
		$source = str_replace( '<$_afterDelete>', $afterDelete, $source );
	
	}
	
	private function toSource_constants( &$source ) {
		$properties = "\n";
		foreach( $this->constants as $const ) {
			$constants .= "\t".$const->code."\n";
		}
		$source = str_replace( '<$constants>', $constants, $source );
	}
	
	
	
	public function save() {

		trace('Parsing class '.$this->name.'','DomAr');
		
		$file = self::getClassFilePath( $this->name );
		
		if( !is_dir(DOMAR_CACHE_DIR_PATH) )
			mkdir(DOMAR_CACHE_DIR_PATH);
	
		if( !is_writable(DOMAR_CACHE_DIR_PATH) )
			throw new RuntimeException('DomAr cache dir is not writable! Check '.DOMAR_CACHE_DIR_PATH);
	
		$this->check();
	
		file_put_contents( $file, $this->toSource() );
		@chmod( $file, 0777);
		//require_once( $file );
		
		//$table = new DomArTable( $this->name );
		//if( $table->exists() === true )
		//	$table->update();
		
	}
	
	public static function getClassFilePath( $className ) {
		return DOMAR_CACHE_DIR_PATH.$className.'.class.php';;
	}
	
	
	
	//--------------------------------------------------------------
	// HELPERS
	//--------------------------------------------------------------
	
	/**
	* Checks if this is instanceOf $className
	* @param String $className
	* @return Boolean
	*/
	public function instance_of( $className ) {
		if( $className == $this->name ) {
			return true;
		} elseif( $this->parentClass ) {
			return $this->parentClass->instance_of( $className );
		} else {
			return false;
		}
	}
	
	/**
	* Checks that listener method actually exist in class.
	* Throws RuntimeException on error.
	* @param String $method
	* @param Array $args
	*/
	public function checkListenerMethod( $method, array $args = array() ) {
		$methods  = $this->getAllMethods();
		$recordMethods = $this->getAllRecordMethods();
		
		if( $methods[$method] )
			$method = $methods[$method];
		elseif( $recordMethods[$method] )
			$method = $recordMethods[$method];
		else
			throw new RuntimeException('Listener method <strong>'.$method.'</strong> doesn\'t exist for class <strong>'.$this->name.'</strong>');
		
		foreach( $args as $i => $arg) {
			if( !in_array($arg, $method->arguments) ) {
				throw new RuntimeException('Listener method <strong>'.$method->name.'</strong> in class <strong>'.$this->name.'</strong> must have <strong>'.($i+1).'</strong> argument! It logical name is <strong>'.$arg.'</strong>');
			}
		}
	}
	
	/**
	* Converts Array to string that could be written into php file.
	* @param Array $array
	* @return String
	*/
	public static function arrayToPhpFunctionArgs( array $array ) {
		$fStr = array();
		foreach( $array as $key => $value) {
			if( is_array($value) === true)
				$fStr[] = (is_numeric($key) && ($key[0] != '0' or strlen($key) == 1) ? $key : "'".$key."'")."=>".self::arrayToPhpFunctionArgs( $value );
			else if( $value === 'NULL' )
				$fStr[] = 'NULL';
			else
				$fStr[] = (is_numeric($key) && ($key[0] != '0' or strlen($key) == 1) ? $key : "'".$key."'")."=>'".$value."'";
		}
		$str = 'array('.implode(',',$fStr).')';
		return $str;
	}
	
	
	
	

}
?>